// Modified or written by Object Mentor, Inc. for inclusion with FitNesse.
// Copyright (c) 2002 Cunningham & Cunningham, Inc.
// Released under the terms of the GNU General Public License version 2 or later.package fit;

package fit;

import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.lang.reflect.*;
import java.text.*;
import fit.exception.*;
import fitlibrary.table.Tables;
import fitlibrary.utility.TableListener;

public class Fixture {
	public Map<String, Object> summary = new HashMap<String, Object>();

	public Counts counts = new Counts();

	public FixtureListener listener = new NullFixtureListener();

	protected String[] args;
	protected String strResponse;
	protected String strErrorMsg;

	private static boolean forcedAbort = false; //Semaphores

	public static void setForcedAbort(boolean state) {
		forcedAbort = state;
	} //Semaphores

	public static boolean getForcedAbort() {
		return forcedAbort;
	} //Semaphores

	public boolean isDoRows = false;

	private static final Pattern symbolPattern = Pattern.compile("([$]([\\w\\W&&[^$]]*)[$](\\s)*[=](\\s)*[\\w\\W]*|"
		+ "([+][+])|" + "([-][-])|" + "((\\s)*[+][=](\\s)*[\\w\\W]*)|" + "((\\s)*[-][=](\\s)*[\\w\\W]*)|"
		+ "((\\s)*[*][=](\\s)*[\\w\\W]*)|" + "((\\s)*[/][=](\\s)*[\\w\\W]*)|" + "[$]([\\w\\W&&[^$]]*)[$]|"
		+ "([$][\\w\\W&&[^$]]*)|([\\w\\W&&[^$]]*))");

	private static final Pattern symbolSubPattern = Pattern.compile("([$][\\w\\W]*[$])");

	private static final Pattern commonPattern = Pattern.compile("([\\w\\W&&[^$]]*)");

	private static final Pattern assignPattern = Pattern.compile("([$][\\w\\W]*[$])((\\s)*[=](\\s)*[\\w\\W]*)");

	private static final Pattern operatorPattern = Pattern.compile("((\\s)*[=](\\s)*[\\w\\W]*)|" + "([+][+])|([-][-])|"
		+ "((\\s)*[+][=](\\s)*[\\w\\W]*)|" + "((\\s)*[-][=](\\s)*[\\w\\W]*)|" + "((\\s)*[*][=](\\s)*[\\w\\W]*)|"
		+ "((\\s)*[/][=](\\s)*[\\w\\W]*)");

	protected Class getTargetClass() {
		return getClass();
	}

	public class RunTime {
		long start = System.currentTimeMillis();

		long elapsed = 0;

		public String toString() {
			elapsed = System.currentTimeMillis() - start;
			if (elapsed > 600000) {
				return d(3600000) + ":" + d(600000) + d(60000) + ":" + d(10000) + d(1000);
			} else {
				return d(60000) + ":" + d(10000) + d(1000) + "." + d(100) + d(10);
			}
		}

		String d(long scale) {
			long report = elapsed / scale;
			elapsed -= report * scale;
			return Long.toString(report);
		}
	}

	// Traversal //////////////////////////
	//Add by Seokmoon Ryoo
	public int runNestedTables(Tables tables, int from, TableListener listener, RunningState childState) {
		return tables.size();
	}

	public int getEndTableIndex(Tables tables, int from) {
		return tables.size();
	}

	/* Altered by Rick to dispatch on the first Fixture */
	public void doTables(Parse tables) {
		summary.put("run date", new Date());
		summary.put("run elapsed time", new RunTime());
		if (tables != null) {
			Parse heading = tables.at(0, 0, 0);

			if (heading != null) {
				try {
					Fixture fixture = getLinkedFixtureWithArgs(tables);
					fixture.listener = listener;
					fixture.interpretTables(tables);
				} catch (Throwable e) {
					exception(heading, e);
					interpretFollowingTables(tables);
				}
			}
		}
		listener.tablesFinished(counts);
		
		SemaphoreFixture.ClearSemaphores(); //Semaphores:  clear all at end
	}



	/* Added by Rick to allow a dispatch into DoFixture */
	protected void interpretTables(Parse tables) {
		try { // Don't create the first fixture again, because creation may do something important.
			getArgsForTable(tables); // get them again for the new fixture object
			doTable(tables);
		} catch (Exception ex) {
			exception(tables.at(0, 0, 0), ex);
			listener.tableFinished(tables);
			return;
		}
		interpretFollowingTables(tables);
	}

	/* Added by Rick */
	public void interpretFollowingTables(Parse tables) {
		listener.tableFinished(tables);
		tables = tables.more;
		while (tables != null) {
			Parse heading = tables.at(0, 0, 0);

			if (forcedAbort)
				ignore(heading); //Semaphores: ignore on failed lock
			else if (heading != null) {
				try {
					Fixture fixture = getLinkedFixtureWithArgs(tables);
					fixture.doTable(tables);
				} catch (Throwable e) {
					exception(heading, e);
				}

			}
			listener.tableFinished(tables);
			tables = tables.more;
		}
	}

	/* Added by Rick */
	protected Fixture getLinkedFixtureWithArgs(Parse tables) throws Throwable {
		Parse header = tables.at(0, 0, 0);
		Fixture fixture = loadFixture(header.text());
		fixture.counts = counts;
		fixture.summary = summary;
		fixture.getArgsForTable(tables);
		return fixture;
	}

	public static Fixture loadFixture(String fixtureName) throws Throwable {
		return FixtureLoader.instance().disgraceThenLoad(fixtureName);
	}

	public void getArgsForTable(Parse table) {
		List<String> argumentList = new ArrayList<String>();
		Parse parameters = table.parts.parts.more;
		for (; parameters != null; parameters = parameters.more) {
			argumentList.add(parameters.text());
		}

		args = (String[])argumentList.toArray(new String[0]);
	}

	public void doTable(Parse table) {
		doRows(table.parts.more);
	}

	public void doRows(Parse rows) {
		while (rows != null) {
			Parse more = rows.more;
			doRow(rows);
			rows = more;
		}
	}

	public void doRow(Parse row) {
		doCells(row.parts);
	}

	public void doCells(Parse cells) {
		for (int i = 0; cells != null; i++) {
			try {
				doCell(cells, i);
			} catch (Exception e) {
				exception(cells, e);
			}
			cells = cells.more;
		}
	}

	public void doCell(Parse cell, int columnNumber) {
		ignore(cell);
	}

	// Annotation ///////////////////////////////
	
	private static final String ITERATION_START = "<!--NTAF_ITERATION_START-->";
	
	private void addTableToCell(Parse cell, String className, String content, boolean upLine) {
		cell.addToBody(ITERATION_START);
		if(upLine)
			cell.addToBody("<hr>");
		cell.addToBody("<div class=" + className + ">" + content + "</div>");
		//cell.addToBody("<table cellpadding=0 cellspacing=0 style=padding:0;margin:0;border:0 width=100%><tr class=" + className + "><td style=padding:5;margin:0;border:0;border-top:" + topBorder + "px>" + content + "</td></tr></table>\n");
	}

	public void right(Parse cell) {

		String resultStr = decorateResultString(cell.text());

		if(cell.isClean()) {
			cell.addToTag(" class=pass");
			cell.body = "";
			if(FitServer.hasSymbol(cell.orgbody))
				cell.addToBody(escape(cell.orgbody));	
			cell.addToBody(escape(resultStr));
		} else {
			if(!cell.body.contains(ITERATION_START)) {
				cell.addToTag(" class=\"\" ");
				//cell.addToTag(" style=padding:0;margin:0 valign=top ");
				String preBody = cell.body;
				cell.body = "";
				addTableToCell(cell, "pass", preBody, false);
			}
			String content = "";
			if(FitServer.hasSymbol(cell.orgbody))
				content = escape(cell.orgbody);
			addTableToCell(cell, "pass", content + escape(resultStr), true);
		}

		counts.right++;
	}

	public void wrong(Parse cell) {
		counts.wrong++;
	}
	
	private static final String NTAFBR = "<!--NTAF_BR--><br>";

	public void wrong(Parse cell, String actual) {
		wrong(cell);

		String resultStr = decorateResultString(actual);
		String orgbody = escape(cell.orgbody);
		
		if(FitServer.hasSymbol(cell.orgbody))
			orgbody +=  "=" + replaceSymbol(cell.orgbody);
		
		String content = orgbody + label("expected &nbsp;") + NTAFBR + escape(resultStr) + label("actual");
		
		if(cell.isClean()) {
			cell.addToTag(" class=fail");
			cell.body = "";
			cell.addToBody(content);
		} else {
			if(!cell.body.contains(ITERATION_START)) {
				cell.addToTag(" class=\"\" ");
				//cell.addToTag(" style=padding:0;margin:0 valign=top ");
				String preBody = cell.body;
				cell.body = "";
				addTableToCell(cell, "fail", preBody.replaceAll(NTAFBR, "") , false);
			}
			addTableToCell(cell, "fail", content.replaceAll(NTAFBR, ""), true);
		}
	}

	private String decorateResultString(String actual) {
		String resultStr = "";

		if (FitServer.hasSymbol(actual)) {
			resultStr = "=" + FitServer.getSymbol(actual);
		} else {
			resultStr = actual;
		}

		return resultStr;
	}

	public void ignore(Parse cell) {
		cell.addToTag(" class=\"ignore\"");
		counts.ignores++;
	}

	public void exception(Parse cell, Throwable exception) {
		while (exception.getClass().equals(InvocationTargetException.class)) {
			exception = ((InvocationTargetException)exception).getTargetException();
		}
		if (isFriendlyException(exception)) {
			cell.addToBody("<hr/>" + label(exception.getMessage()));
		} else {
			final StringWriter buf = new StringWriter();
			exception.printStackTrace(new PrintWriter(buf));
			cell.addToBody("<hr><pre><div class=\"fit_stacktrace\">" + (buf.toString()) + "</div></pre>");
		}
		cell.addToTag(" class=\"error\"");
		counts.exceptions++;
	}

	public boolean isFriendlyException(Throwable exception) {
		return exception instanceof FitFailureException;
	}

	// Utility //////////////////////////////////

	public String counts() {
		return counts.toString();
	}

	public static String label(String string) {
		return " <span class=\"fit_label\">" + string + "</span>";
	}

	public static String gray(String string) {
		return " <span class=\"fit_grey\">" + string + "</span>";
	}

	public static String escape(String string) {
		return escape(escape(string, '&', "&amp;"), '<', "&lt;");
	}

	public static String escape(String string, char from, String to) {
		int i = -1;
		while ((i = string.indexOf(from, i + 1)) >= 0) {
			if (i == 0) {
				string = to + string.substring(1);
			} else if (i == string.length()) {
				string = string.substring(0, i) + to;
			} else {
				string = string.substring(0, i) + to + string.substring(i + 1);
			}
		}
		return string;
	}

	public static String camel(String name) {
		StringBuffer b = new StringBuffer(name.length());
		StringTokenizer t = new StringTokenizer(name);
		b.append(t.nextToken());
		while (t.hasMoreTokens()) {
			String token = t.nextToken();
			b.append(token.substring(0, 1).toUpperCase()); // replace spaces with
			// camelCase
			b.append(token.substring(1));
		}
		return b.toString();
	}

	public Object parse(String s, Class type) throws Exception {
		if (type.equals(String.class)) {
			if (s.toLowerCase().equals("null"))
				return null;
			else if (s.toLowerCase().equals("blank"))
				return "";
			else
				return s;
		} else if (type.equals(Date.class)) {
			return DateFormat.getDateInstance(DateFormat.SHORT).parse(s);
		} else if (hasParseMethod(type)) {
			return callParseMethod(type, s);
		} else {
			throw new CouldNotParseFitFailureException(s, type.getName());
		}
	}

	public void check(Parse cell, TypeAdapter a) {
		String text = replaceSymbol(cell.text());

		if (text.equals(""))
			handleBlankCell(cell, a);
		else if (a == null)
			ignore(cell);
		else if (text.equals("error"))
			handleErrorInCell(a, cell);
		else
			compareCellToResult(a, cell);
	}

	private void compareCellToResult(TypeAdapter a, Parse cell) {
		new CellComparator().compareCellToResult(a, cell);
	}

	public void handleBlankCell(Parse cell, TypeAdapter a) {
		try {
			if (cell.isClean()) {
				cell.body = "";
				String strValue = a.toString(a.get());
				cell.addToBody(gray(strValue.trim()));
			} else
				cell.addToBody("<hr>" + gray(a.toString(a.get())));
		} catch (Exception e) {
			if (cell.isClean())
				cell.addToBody(gray("error"));
			else
				cell.addToBody("<hr>" + gray("error"));

		}
	}

	private void handleErrorInCell(TypeAdapter a, Parse cell) {
		try {
			Object result = a.invoke();
			wrong(cell, a.toString(result));
		} catch (IllegalAccessException e) {
			exception(cell, e);
		} catch (Exception e) {
			right(cell);
		}
	}

	public String[] getArgs() {
		return args;
	}

	public static boolean hasParseMethod(Class type) {
		try {
			type.getMethod("parse", new Class[] {String.class});
			return true;
		} catch (NoSuchMethodException e) {
			return false;
		}
	}

	public static Object callParseMethod(Class type, String s) throws Exception {
		Method parseMethod = type.getMethod("parse", new Class[] {String.class});
		Object o = parseMethod.invoke(null, new Object[] {s});
		return o;
	}

	public static String executeOperator(String operator, String symbol) {
		String operatorValue = operator.trim();
		String symbolValue = symbol.trim();
		String result;
		
		if (isNumberOperation(operatorValue, symbolValue)) {
			result = getNumerResult(operatorValue, Double.parseDouble(symbolValue));
		} else {
			result = getStringResult(operatorValue, symbolValue);
		}

		return result;
	}

	private static String getStringResult(String operatorValue, String symbolValue) {
		String result = symbolValue;

		String calulationValue = getCalulationValue(operatorValue);

		if (operatorValue.contains("++") && 1 <= result.length()) {
			result += result.charAt(0);
		} else if (isStringDecrementable(operatorValue, result)) {
			result = result.subSequence(0, result.length() - 1).toString();
		} else if (operatorValue.contains("+=")) {
			result += calulationValue;
		} else if (operatorValue.contains("-=")) {
			result = result.replace(calulationValue, "");
		} else if (operatorValue.contains("*=")) {
			result = getTimeOperationResult(result, calulationValue);
		} else if (operatorValue.contains("/=")) {
			result = result.replaceAll(calulationValue, "");
		}

		return result;
	}

	private static boolean isStringDecrementable(String operatorValue, String result) {
		return operatorValue.contains("--")
				&& result.length() >= 1;
	}

	private static String getTimeOperationResult(String input, String calulationValue) {
		String result = input;
		
		if (isNumberOperation(calulationValue, calulationValue)) {
			String addString = result;
			for (long i = 0; i < Long.parseLong(calulationValue); ++i) {
				result += addString;
			}
		} else {
			result += calulationValue;
		}
		return result;
	}

	private static boolean isNumberOperation(String operatorValue, String symbolValue) {
		boolean isNumber = false;
		try {
			Double.parseDouble(symbolValue);
			if (operatorValue.length() > 2) {
				Double.parseDouble(getCalulationValue(operatorValue));
			}

			isNumber = true;
		} catch (NumberFormatException e) {
			isNumber = false;
		}

		return isNumber;
	}

	private static String getNumerResult(String operatorValue, double value) {
		double resultValue = value;

		if (operatorValue.contains("++")) {
			resultValue++;
		} else if (operatorValue.contains("--")) {
			resultValue--;
		} else if (operatorValue.contains("+=")) {
			resultValue += Double.parseDouble(getCalulationValue(operatorValue));
		} else if (operatorValue.contains("-=")) {
			resultValue -= Double.parseDouble(getCalulationValue(operatorValue));
		} else if (operatorValue.contains("*=")) {
			resultValue *= Double.parseDouble(getCalulationValue(operatorValue));
		} else if (operatorValue.contains("/=")) {
			resultValue /= Double.parseDouble(getCalulationValue(operatorValue));
		}

		return String.valueOf(resultValue);
	}

	private static String getCalulationValue(String argumentValue) {
		if (argumentValue.length() < 3) {
			return "";
		}
		
		String result = argumentValue.substring(2).trim();
		
		if (FitServer.hasSymbol(result)) {
			result = replaceSymbol(result);
		}

		return result;
	}

	public static synchronized String replaceSymbol(String str) {
		StringBuffer result = new StringBuffer();
		String buffer = "";
		String symbol = "";

		Matcher m1 = symbolPattern.matcher(str);

		while (m1.find()) {
			Matcher m2 = assignPattern.matcher(m1.group());
			if (m2.matches()) {
				StringTokenizer stok = new StringTokenizer(m2.group(), "= \t", false);
				while (stok.hasMoreTokens()) {
					if (!symbol.equals("")) {
						result.append(stok.nextToken());
						FitServer.setSymbol(symbol, result);
					} else
						symbol = stok.nextToken();
				}
			}

			m2 = symbolSubPattern.matcher(m1.group());
			if (m2.matches()) {
				symbol = m2.group();
				Object returnSymbol = FitServer.getSymbol(symbol);
				if (null != returnSymbol) {
					buffer = returnSymbol.toString().trim();
				} else {
					buffer = symbol;
				}
			}

			m2 = operatorPattern.matcher(m1.group());
			if (m2.matches()) {
				result.append(executeOperator(m2.group(), buffer));
				FitServer.setSymbol(symbol, result);
				symbol = "";
				buffer = "";
			} else {
				m2 = commonPattern.matcher(m1.group());
				if (m2.matches()) {
					result.append(buffer + m1.group());
					buffer = "";
				}
			}
		}
		return result.toString();
	}

	// TODO-RcM I might be moving out of here. Can you help me find a home of my
	// own?
	private class CellComparator {
		private Object result = null;

		private Object expected = null;

		private TypeAdapter typeAdapter;

		private Parse cell;

		private void compareCellToResult(TypeAdapter a, Parse theCell) {
			typeAdapter = a;
			cell = theCell;

			try {
				result = typeAdapter.get();
				expected = parseCell();

				if (expected instanceof Unparseable)
					tryRelationalMatch();
				else
					compare();
			} catch (Exception e) {
				exception(cell, e);
			}
		}

		private void compare() {
			prepareCompare();

			if (typeAdapter.equals(expected, result)) {
				right(cell);
			} else {
				wrong(cell, typeAdapter.toString(result));
			}
		}

		private void prepareCompare() {
			if (expected instanceof String && FitServer.hasSymbol(expected.toString()))
				expected = replaceSymbol(expected.toString());

			if (result instanceof String && FitServer.hasSymbol(result.toString())) {
				result = replaceSymbol(result.toString());
			}
		}

		private Object parseCell() {
			try {
				String symbolResolved = replaceSymbol(cell.text());
				return typeAdapter.isRegex ? cell.text() : typeAdapter.parse(symbolResolved);
			}
			// Ignore parse exceptions, print non-parse exceptions,
			// return null so that compareCellToResult tries relational matching.
			catch (NumberFormatException e) {
			} catch (ParseException e) {
			} catch (Exception e) {
				e.printStackTrace();
			}
			return new Unparseable();
		}

		private void tryRelationalMatch() {
			Class adapterType = typeAdapter.type;
			FitFailureException cantParseException = new CouldNotParseFitFailureException(cell.text(),
				adapterType.getName());
			if (result != null) {
				FitMatcher matcher = new FitMatcher(cell.text(), result);
				try {
					if (matcher.matches())
						right(cell);
					else
						wrong(cell);
					cell.body = matcher.message();
				} catch (FitMatcherException fme) {
					exception(cell, cantParseException);
				} catch (Exception e) {
					exception(cell, e);
				}
			} else {
				// TODO-RcM Is this always accurate?
				exception(cell, cantParseException);
			}
		}
	}

	private class Unparseable {
	}
}